package org.bhf.json;

/**
 * Copyright (c) 2007, Blue Hole Software. All rights reserved.
 * Code licensed under the Apache 2.0 License:
 * http://www.apache.org/licenses/
 */

import javassist.ClassPool;
import javassist.LoaderClassPath;

import java.io.IOException;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

/**
 * Utilities private to the JSON writer/reader package.
 * Do not user. Artificially public.
 *
 * @exclude
 */
public final class Util
{
    public static final String  generatedPackage = "org.bhf.json.generated";

    static ClassPool pool = null;

    /**
     * Manage our own class pool
     *
     * @return The Ct Class Pool
     */
    static synchronized ClassPool   getClassPool()
    {
        if( pool == null )
        {
            final ClassPool p = new ClassPool();
            p.appendSystemPath();
            p.appendClassPath( new LoaderClassPath( Thread.currentThread().getContextClassLoader() ) );
            pool = p;
        }

        return pool;
    }


    public static Type[] getMapComponentTypes( final Type collectionType )
        throws IOException // Will blow up if not a Map
    {
        if( collectionType instanceof ParameterizedType )
        {
            return ParameterizedType.class.cast( collectionType ).getActualTypeArguments();
        }
        else
        {
            return new Type[] { Object.class, Object.class };
        }
    }

    public static Collection<PropertyInfo>  getPropertyInfo( final Class clazz )
    {
        final Map<String,PropertyInfo> infos = new HashMap<String,PropertyInfo>();
        final Method[] methods = clazz.getMethods();

        for( final Method method : methods )
        {
            if( isGetter( method ) )
            {
                final String p = methodNameToProperty( method );
                PropertyInfo info = infos.get( p );
                if( info == null )
                {
                    info = new PropertyInfo( method, null, method.getGenericReturnType(), p );
                    infos.put( p, info );
                }
                else
                {
                    if( info.getGetter() != null || info.getSetter() == null )
                        throw new RuntimeException( "The property " + p + " has more than one getter method" );
                    info.setGetter( method );
                    if( !info.getGetter().getGenericReturnType().equals( info.getSetter().getGenericParameterTypes()[0] ) )
                        throw new RuntimeException(
                            "getter/setter type mismatch for " + clazz.getName() + " property " + info.getProperty()
                        );
                }
            }
            else if( isSetter( method ) )
            {
                final String p = methodNameToProperty( method );
                PropertyInfo info = infos.get( p );
                if( info == null )
                {
                    info = new PropertyInfo( null, method, method.getGenericParameterTypes()[0], p );
                    infos.put( p, info );
                }
                else
                {
                    if( info.getSetter() != null || info.getGetter() == null )
                        throw new RuntimeException( "The property " + p + " has more than one setter method" );
                    info.setSetter( method );
                    if( !info.getGetter().getGenericReturnType().equals( info.getSetter().getGenericParameterTypes()[0] ) )
                        throw new RuntimeException(
                            "getter/setter type mismatch for " + clazz.getName() + " property " + info.getProperty()
                        );
                }
            }
        }

        for( Iterator<Map.Entry<String,PropertyInfo>> i =  infos.entrySet().iterator(); i.hasNext(); )
        {
            final Map.Entry<String,PropertyInfo> info = i.next();
            if( JSONTransient.Util.isTransient( info.getValue().getGetter() ) )
                i.remove();
        }

        return infos.values();
    }

    public static boolean   isGetter( final Method getter )
    {
        final String name           = getter.getName();

        boolean ok;

        ok = name.startsWith( "get" ) || name.startsWith( "is" );
        ok = ok && !getter.getDeclaringClass().equals( Object.class ); // Don't want Object stuff
        ok = ok && Modifier.isPublic( getter.getModifiers() );
        ok = ok && !Modifier.isStatic( getter.getModifiers() );
        ok = ok && !Modifier.isVolatile( getter.getModifiers() );
        ok = ok && getter.getParameterTypes().length == 0;

        return ok;
    }

    public static boolean   isSetter( final Method setter )
    {
        final String name           = setter.getName();

        boolean ok;

        ok = name.startsWith( "set" );
        ok = ok && !setter.getDeclaringClass().equals( Object.class ); // Don't want Object stuff
        ok = ok && Modifier.isPublic( setter.getModifiers() );
        ok = ok && !Modifier.isStatic( setter.getModifiers() );
        ok = ok && !Modifier.isVolatile( setter.getModifiers() );
        ok = ok && setter.getParameterTypes().length == 1;

        return ok;
    }

    public static String    methodNameToProperty( final Method getter )
    {
        final String name = getter.getName();

        if( name.startsWith( "set" ) )
            return Character.toLowerCase( name.charAt( 3 ) ) + name.substring( 4 );
        else if( name.startsWith( "get" ) )
            return Character.toLowerCase( name.charAt( 3 ) ) + name.substring( 4 );
        else if( name.startsWith( "is" ) )
            return Character.toLowerCase( name.charAt( 2 ) ) + name.substring( 3 );
        else
            return name;
    }

    /**
     * Return the boxing component class having a valueOf() method for the given primitive.
     *
     * @param primitive A primitive type
     * @return The appropriate boxing class
     * @throws RuntimeException if primitive cannot be boxed
     */
    public static Class     boxingComponentClass( final Class primitive )
    {
        if( primitive.equals( Integer.TYPE ) )
            return Integer.class;
        if( primitive.equals( Boolean.TYPE ) )
            return Boolean.class;
        if( primitive.equals( Long.TYPE ) )
            return Long.class;
        if( primitive.equals( Float.TYPE ) )
            return Float.class;
        if( primitive.equals( Character.TYPE ) )
            return Character.class;
        if( primitive.equals( Byte.TYPE ) )
            return Byte.class;
        if( primitive.equals( Short.TYPE ) )
            return Short.class;
        if( primitive.equals( Double.TYPE ) )
            return Double.class;
        throw new RuntimeException( primitive.getName() + " is not a boxable primitive" );
    }


    public static Type getCollectionComponentType( final Type collectionType ) // Will blow up if not a Collection
    {
        if( collectionType instanceof ParameterizedType )
        {
            return ParameterizedType.class.cast( collectionType ).getActualTypeArguments()[0];
        }
        else if( collectionType instanceof GenericArrayType )
        {
            return GenericArrayType.class.cast( collectionType ).getGenericComponentType();
        }
        else
        {
            return Object.class; // Non-generic collection: pretty much useless
        }
    }
    
    /**
     * Recursively descend nested parameterization until we found the underlying raw type.
     *
     * @param type Must not be <code>null</code>
     * @return The raw type
     * @throws UnsupportedTypeException Never found a raw type
     */
    public static Class     digRawType( final Type type )
        throws UnsupportedTypeException
    {
        if( type instanceof Class )
            return Class.class.cast( type );
        else if( type instanceof ParameterizedType )
            return digRawType( ParameterizedType.class.cast( type ).getRawType() );
        else
            throw new UnsupportedTypeException( type );
    }

    /**
     * Convert a stack of properties and indices to a property path.
     *
     * @param stack a stack of properties and indices
     * @return a property path
     */
    public static String    stackToPath( final List<String> stack )
    {
        final StringBuffer  s               = new StringBuffer();
              boolean       needsDelimiter  = false;

        for( final String p : stack )
        {
            try
            {
                Integer.parseInt( p );
                s.append( '[' );
                s.append( p );
                s.append( ']' );
            }
            catch( NumberFormatException nfe )
            {
                if( needsDelimiter )
                    s.append( '.' );
                s.append( p );
            }
            needsDelimiter = true;
        }

        return s.toString();
    }

    /**
     * Return a unique class name for the writer of this particular type. In general, the name will be different
     * during each runtime.
     *
     * @param type Must not be <code>null</code>
     * @return Class name for the writer
     */
    public static String   getReaderClassNameForType( final Type type )
    {
        return generatedPackage + ".JSONParser_" + typeToIdentifier( type );
    }

    /**
     * Return a unique class name for the writer of this particular type. In general, the name will be different
     * during each runtime.
     *
     * @param type Must not be <code>null</code>
     * @return Class name for the writer
     */
    public static String   getWriterClassNameForType( final Type type )
    {
        return generatedPackage + ".JSONWriter_" + typeToIdentifier( type );
    }

    private static String   typeToIdentifier( final Type type )
    {
        final String        raw     = getDeclarationName( type, false );
        final StringBuffer  clean   = new StringBuffer();

        for( int i = 0; i < raw.length(); i++ )
        {
            final char c = raw.charAt( i );
            switch( c )
            {
                case '1':   clean.append( "10" );   break;
                case '<':   clean.append( "11" );   break;
                case '>':   clean.append( "12" );   break;
                case '[':   clean.append( "13" );   break;
                case ']':   clean.append( "14" );   break;
                case '.':   clean.append( "15" );   break;
                case '$':   clean.append( "16" );   break;
                case ';':   clean.append( "17" );   break;
                case ',':   clean.append( "18" );   break;
                default:    clean.append( c );      break;
            }
        }

        return clean.toString();
    }

    /**
     * Given a <code>Type</code>, figure out how it is declared in the java language. The declaration will
     * be unique for the given type, which may be a pojo, array, generic collection, etc. This signature
     * is used to key the writer/reader collections and when generating variable declartions in generated
     * code. In the latter case, the generics must be suppressed since the language level used to
     * generate code does not support generics.
     *
     * @param type Must not be <code>null</code>
     * @param suppressGenerics <code>true</code> to suppress the generic sections of the declaration
     * @return The type declaration
     */
    public static String   getDeclarationName( final Type type, final boolean suppressGenerics )
    {
        if( type instanceof Class )
        {
            final Class clazz = Class.class.cast( type );

            if( clazz.isArray() )
            {
                final String name = clazz.getName();

                final int depth = name.lastIndexOf( '[' ) + 1;
                final StringBuffer s = new StringBuffer();

                if( name.charAt( depth ) == 'L' ) // Object
                {
                    s.append( name.substring( depth + 1, name.length() - 1 ) );
                }
                else // primitive
                {
                    switch( name.charAt( depth ) )
                    {
                        case 'Z': s.append( "boolean"); break;
                        case 'B': s.append( "byte"); break;
                        case 'C': s.append( "char"); break;
                        case 'D': s.append( "double"); break;
                        case 'F': s.append( "float"); break;
                        case 'I': s.append( "int"); break;
                        case 'J': s.append( "long"); break;
                        case 'S': s.append( "short"); break;
                    }
                }

                for( int i = 0; i < depth; i++ )
                    s.append( "[]" );

                return s.toString();
            }
            else
            {
                return clazz.getName();
            }
        }
        else if( type instanceof GenericArrayType ) // Generic
        {
            final GenericArrayType anArrayType = GenericArrayType.class.cast( type );

            return getDeclarationName( anArrayType.getGenericComponentType(), suppressGenerics ) + "[]";
        }
        else if( type instanceof ParameterizedType ) // Generic
        {
            final ParameterizedType pType = ParameterizedType.class.cast( type );
            final Type[] types = pType.getActualTypeArguments();
            final StringBuffer s = new StringBuffer();

            s.append( getDeclarationName( pType.getRawType(), suppressGenerics ) );
            // Code Generation Library does not yet support generics - gotta fake it
            if( !suppressGenerics )
            {
                s.append( '<' );
                for( int i = 0; i < types.length; i++ )
                {
                    if( i > 0 )  s.append( ',' );
                    s.append( getDeclarationName( types[i], suppressGenerics ) );
                }
                s.append( '>' );
            }

            return s.toString();
        }
        else
        {
            // Recursive exception!
            // throw new RuntimeException( new UnsupportedTypeException( type ) );
            throw new RuntimeException( "Bad Type (perhaps not a concrete class): " + (type != null ? type : "null") );
        }
    }

    public static String    escape( final String string )
    {

        if( string.length() == 0 ) return "\"\"";

        char b;
        char c = 0;
        int i;
        int len = string.length();
        StringBuffer sb = new StringBuffer( len + 4 );
        String t;

        sb.append( '"' );
        for( i = 0; i < len; i += 1 )
        {
            b = c;
            c = string.charAt( i );
            switch( c )
            {
                case'\\':
                case'"':
                    sb.append( '\\' );
                    sb.append( c );
                    break;
                case'/':
                    if( b == '<' )
                    {
                        sb.append( '\\' );
                    }
                    sb.append( c );
                    break;
                case'\b':
                    sb.append( "\\b" );
                    break;
                case'\t':
                    sb.append( "\\t" );
                    break;
                case'\n':
                    sb.append( "\\n" );
                    break;
                case'\f':
                    sb.append( "\\f" );
                    break;
                case'\r':
                    sb.append( "\\r" );
                    break;
                default:
                    if( c < ' ' )
                    {
                        t = "000" + Integer.toHexString( c );
                        sb.append( "\\u" )
                            .append( t.substring( t.length() - 4 ) );
                    }
                    else
                    {
                        sb.append( c );
                    }
            }
        }
        sb.append( '"' );

        return sb.toString();
    }

    /**
     * @exclude
     */
    public static class PropertyInfo
    {
        private Method  getter;
        private Method  setter;
        private Type    type;
        private String  property;
        private boolean hashConflict;

        PropertyInfo( Method getter, Method setter, Type type, String property )
        {
            this.getter = getter;
            this.setter = setter;
            this.type = type;
            this.property = property;
        }

        void setHashConflict( boolean hashConflict )
        {
            this.hashConflict = hashConflict;
        }

        public boolean isHashConflict()
        {
            return hashConflict;
        }

        void setGetter( Method getter )
        {
            this.getter = getter;
        }

        void setSetter( Method setter )
        {
            this.setter = setter;
        }

        void setType( Type type )
        {
            this.type = type;
        }

        void setProperty( String property )
        {
            this.property = property;
        }

        public Method getGetter()
        {
            return getter;
        }

        public Method getSetter()
        {
            return setter;
        }

        public Type getType()
        {
            return type;
        }

        public String getProperty()
        {
            return property;
        }
    }
}
